Skip to content

Update module github.com/labstack/echo/v4 to v4.16.1 [SECURITY] - #85

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/go-github.com-labstack-echo-v4-vulnerability
Open

renovate[bot] wants to merge 1 commit into
mainfrom
renovate/go-github.com-labstack-echo-v4-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
github.com/labstack/echo/v4 v4.15.0 → v4.16.1 age confidence

Echo: Encoded slash (%2F) bypasses route-level protection and exposes static files

CVE-2026-55677 / GHSA-vfp3-v2gw-7wfq

More information

Details

Summary

Echo's router and static file handler disagree on URL path decoding. The router matches routes using the raw encoded path (preserving %2F as-is), while StaticDirectoryHandler unescapes %2F to / before resolving filesystem paths. This allows an attacker to bypass route-level access controls and read static files without authorization.

Details

Root cause 1 — router.go lines 798-802:
The router uses req.URL.RawPath for route matching when useEscapedPathForRouting is false (the default). This means /admin%2Fsecret.txt is treated as a single path segment and does NOT match the /admin/* route pattern.

if !r.useEscapedPathForRouting && req.URL.RawPath != "" {
    path = req.URL.RawPath
}

Root cause 2 — echo.go lines 559-568:
StaticDirectoryHandler calls url.PathUnescape() on the path parameter before opening files. This converts %2F back to /, resolving admin/secret.txt on disk.

if !disablePathUnescaping {
    tmpPath, err := url.PathUnescape(p)
    p = tmpPath
}
name := filepath.ToSlash(filepath.Clean(strings.TrimPrefix(p, "/")))
PoC (Screenshot)

Sample:
image

403:
image

Bypass with encoded slash:
image

Impact

Unauthorized static file disclosure. Applications that protect route prefixes with authentication middleware while also serving static files from a broader root are vulnerable. An attacker only needs to encode the slash (/ → %2F) in the URL to bypass all route-level protection.

Common affected pattern:

adminGroup := e.Group("/admin", authMiddleware)
e.StaticFS("/", os.DirFS("public"))

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Echo: WebSocket proxy can retain connections and handlers after client disconnect

CVE-2026-118262 / GHSA-9969-6676-49h5

More information

Details

Impact

Echo's Proxy middleware could retain a handler, copy goroutines and network connections after a WebSocket client disconnected if the upstream kept its connection open. The raw proxy waited for both copy directions to finish without reliably closing the other connection when one direction ended. Rejected upgrade responses with HTTP keep-alive were one way to trigger this behavior.

Repeated connections can accumulate resources and cause denial of service. The impact depends on the proxy route's accessibility, upstream behavior and configured connection deadlines. This is a medium-severity resource-retention issue; no confidentiality or integrity impact is claimed.

Affected versions and patch

Affected versions are v4.15.0 through v4.16.0 and v5.0.0 through v5.4.0. The regression was introduced by commit e2133320c7, merged in #​2852.

Upgrade to v4.16.1 or v5.4.1. The patch uses Go's HTTP reverse proxy, preserves ordinary HTTP handling for rejected upgrades, and closes both sides of a WebSocket tunnel when either direction ends. It also covers an upstream that receives client EOF but deliberately keeps its side open.

Fix commits: v4, v5.

Workarounds

Disable WebSocket proxy routes until upgrading, or apply bounded connection lifetimes and resource limits at the frontend and upstream. Server idle timeouts alone may not cover hijacked WebSocket connections.

Credits

Reported by @​irisitymichaelgrundberg.

Severity

  • CVSS Score: 8.2 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Echo: BodyDump captures request and response bodies without size limits in v4

CVE-2026-118260 / GHSA-f48p-c9v4-h7xv

More information

Details

Impact

Echo v4's opt-in BodyDump middleware read complete request bodies into memory and retained complete response bodies without a capture limit. A client able to submit large requests to a route using BodyDump could cause large allocations before the application handler ran. Large responses, including responses whose size can be influenced by a client, also caused an additional unbounded capture allocation.

Repeated or concurrent requests can exhaust process memory and cause denial of service. BodyDump is not enabled by default. Deployments with effective request limits may reduce the request-side exposure; response capture requires separate consideration.

Affected versions and patch

Echo v4 versions through v4.16.0 are affected. Upgrade to v4.16.1.

The patch adds BodyDumpConfig.MaxRequestBytes and MaxResponseBytes, each defaulting to 5 MiB when zero. A value of -1 explicitly restores unlimited capture. The complete request remains readable by the handler and the complete response is delivered to the client; only the callback's captured data is bounded. Request close behavior and read errors are preserved.

Fix commit.

Workarounds

Disable BodyDump on exposed routes, particularly large uploads and downloads. Alternatively, enforce body-size limits before BodyDump and independently bound the responses that the application generates.

Credits

Reported by @​q490107865. The patch also incorporates request-body preservation work from @​jakezwang in #​3105.

Severity

  • CVSS Score: 8.2 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Echo: Decompress permits unbounded gzip expansion in v4

CVE-2026-118261 / GHSA-h7wr-284f-5qvc

More information

Details

Impact

Echo v4's opt-in Decompress middleware exposed a gzip-decoded request stream without limiting its expanded size. A small compressed request could therefore produce a very large decoded body. Handlers that read or bind that body could consume excessive memory or CPU, and repeated or concurrent requests could cause denial of service.

The middleware is not enabled by default. A limit on the compressed request size alone does not bound the decompressed size. Applications that already apply an effective body limit after decompression have reduced exposure.

Affected versions and patch

Echo v4 versions through v4.16.0 are affected. Upgrade to v4.16.1.

The patch adds DecompressConfig.MaxDecompressedSize, defaulting to 100 MiB when zero. A value of -1 explicitly disables the limit. Reading beyond the cap returns ErrStatusRequestEntityTooLarge (HTTP 413), including through JSON and XML binding. Bodies exactly at the configured limit remain valid. Streaming enforcement requires callers to handle body-read errors.

Fix commit.

Workarounds

Disable gzip request decompression on exposed routes, or place BodyLimit after Decompress so the application limit applies to decoded bytes. Apply suitable request deadlines and concurrency limits as well.

Credits

Reported by @​q490107865. The exact-limit behavior incorporates @​Shubham-Padkonde's work in #​3136.

Severity

  • CVSS Score: 8.2 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Echo: Rejected WebSocket upgrades can bypass Echo route-level access controls

CVE-2026-118264 / GHSA-cxf3-9h37-7rr8

More information

Details

Impact

Echo's Proxy middleware selected a raw tunnel when the client requested a WebSocket upgrade, before validating whether the configured upstream accepted it. If the upstream returned an ordinary HTTP response and kept the connection alive, subsequent HTTP requests on that connection were forwarded directly to the same upstream without passing through Echo's router or route-specific middleware.

This can bypass authentication or authorization enforced only at Echo's routes. Exploitation requires an accessible proxy route, an upstream that rejects the upgrade without closing the connection, and a target resource on that upstream without independent authorization. The issue does not provide arbitrary-host proxying or bypass the upstream's own authentication.

Affected versions and patch

The raw proxy implementation is present throughout the v4 and v5 release lines up to v4.16.0 and v5.4.0. Runtime reproductions confirmed v4.15.0, v4.16.0, v5.0.0 and v5.4.0; source history also shows the missing upgrade-response validation in v4.0.0.

Upgrade to v4.16.1 or v5.4.1. The patch routes WebSocket upgrades through Go's HTTP reverse proxy and enters a tunnel only after a valid upstream upgrade response. Rejected upgrades remain ordinary HTTP requests.

Fix commits: v4, v5.

Workarounds

Reject WebSocket upgrade requests before they reach affected Echo proxy routes, or enforce authorization independently at every upstream resource. Merely returning a non-101 response from the upstream does not prevent the vulnerable raw tunnel.

Credits

Reported by @​Duodum.

Severity

  • CVSS Score: 9.1 / 10 (Critical)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Echo has CSRF Protection Bypass via MethodOverride Rewriting POST to Safe Method

CVE-2026-118266 / GHSA-r7w9-592q-9vg4

More information

Details

Summary

The MethodOverride middleware accepts any non-empty string as the override target method — including safe methods (GET, HEAD, OPTIONS, TRACE) — when rewriting a POST request. When MethodOverride is registered before CSRF in the post-routing middleware chain (e.Use()), it rewrites the request method to a safe method before CSRF evaluates it. The CSRF middleware then skips token validation for the now-safe method, allowing state-changing POST handlers to execute without any CSRF check. Both the legacy token path and the modern Sec-Fetch-Site protection path are bypassed simultaneously.

Affected Component
  • middleware/method_override.go — MethodOverrideConfig.ToMiddleware() (lines 55–71): no whitelist on the override target method
  • middleware/csrf.go — CSRFConfig.ToMiddleware() (lines 188–189): safe-method bypass branch reads the already-rewritten method
  • middleware/csrf.go — checkSecFetchSiteRequest() (line 283): Sec-Fetch-Site safe check also reads the rewritten method
Description
MethodOverride accepts safe methods as override targets

MethodOverrideConfig.ToMiddleware() (method_override.go:55-71) restricts the source method (only POST requests are overrideable) but applies no whitelist to the target:

// method_override.go:61-67
req := c.Request()
if req.Method == http.MethodPost {
    m := config.Getter(c)
    if m != "" {
        req.Method = m   // ← any non-empty string accepted, including "GET"
    }
}

The docstring's security claim — "For security reasons, only POST method can be overridden" — refers only to the source-method restriction. There is no guard preventing POST from being rewritten to GET, HEAD, OPTIONS, or TRACE. The test suite (method_override_test.go) only tests POST→DELETE and POST→PUT; POST→GET is never tested or blocked.

CSRF safe-method branch reads the already-rewritten method

CSRFConfig.ToMiddleware() (csrf.go:188-189) makes its validation decision based on c.Request().Method at the time it runs:

// csrf.go:188-189
switch c.Request().Method {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
    // skip validation — no token check
default:
    // validate CSRF token

If MethodOverride has already run and set req.Method = "GET", this switch falls into the skip branch regardless of the original HTTP method or what handler was matched.

The Sec-Fetch-Site-based path in checkSecFetchSiteRequest() (csrf.go:283) is identically vulnerable:

// csrf.go:283
isSafe := slices.Contains(safeMethods, c.Request().Method)

With req.Method = "GET" (already rewritten), isSafe = true and the function returns true — the request is allowed even when Sec-Fetch-Site is cross-site. Both CSRF protection paths are bypassed in a single step.

Execution chain

In echo.go:700, routing and middleware wrapping are performed in one step:

h = applyMiddleware(e.router.Route(c), e.middleware...)
  1. e.router.Route(c) evaluates with req.Method = "POST" → matches and binds the POST /transfer handler
  2. applyMiddleware wraps it: MethodOverride → CSRF → postHandler

At request execution time:
3. MethodOverride runs: req.Method == "POST" → reads form field _method = "GET" → req.Method = "GET" → calls next
4. CSRF runs: c.Request().Method = "GET" → case http.MethodGet → skips token validation → calls next
5. postHandler runs (the handler matched in step 1 — the original POST /transfer state-change handler)

The state-changing handler executes with no CSRF token validation and with no Sec-Fetch-Site check.

The missing target-method whitelist is the root cause

The legitimate use case for MethodOverride is allowing HTML forms (which only support GET/POST) to send PUT, DELETE, or PATCH requests. There is no legitimate scenario in which an HTML form needs to rewrite POST to GET — that can always be expressed as an ordinary GET request. Allowing POST→GET (and HEAD/OPTIONS/TRACE) serves no valid purpose and is inherently dangerous when any safe-method-aware security middleware (CSRF, logging, audit) appears downstream.

Proof of Concept
package main

import (
    "net/http"
    echo "github.com/labstack/echo/v5"
    "github.com/labstack/echo/v5/middleware"
)

func main() {
    e := echo.New()
    // MethodOverride before CSRF — both in e.Use() (post-routing)
    e.Use(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
        Getter: middleware.MethodFromForm("_method"),
    }))
    e.Use(middleware.CSRF())
    e.POST("/transfer", func(c *echo.Context) error {
        return c.String(http.StatusOK, "transfer executed")
    })
    e.Logger.Fatal(e.Start(":18080"))
}
##### Baseline — CSRF correctly blocks a POST without a valid token
curl -i -X POST 'http://127.0.0.1:18080/transfer' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'amount=100'

##### HTTP/1.1 400 Bad Request (missing CSRF token)

##### Bypass — _method=GET rewrites method before CSRF runs, skipping token validation
curl -i -X POST 'http://127.0.0.1:18080/transfer' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data '_method=GET&amount=100'

##### HTTP/1.1 200 OK
##### transfer executed

##### Browser-based CSRF PoC (attacker's page, auto-submits on load):

##### <form method="POST" action="https://victim.example/transfer" id="f">
#####   <input name="_method" value="GET">

#####   <input name="amount" value="1000">
#####   <input name="to" value="attacker_account">

##### </form>
##### <script>document.getElementById('f').submit();</script>

The same bypass works with MethodFromHeader (X-HTTP-Method-Override: GET) and MethodFromQuery (?_method=GET).

Impact
  • Any state-changing endpoint protected by CSRF() is reachable without a valid CSRF token when the vulnerable middleware ordering is present
  • The Sec-Fetch-Site modern-browser CSRF protection is bypassed simultaneously — browsers that support Sec-Fetch-Site provide no additional defense
  • Applicable attacks include: fund transfers, password/email changes, account deletion, privilege escalation, data manipulation — any POST-protected operation
  • Audit logs may record GET as the HTTP method for what were actually state-changing operations
Recommended Remediation
Option 1: Whitelist only unsafe methods as override targets in MethodOverride (preferred)

The root cause is that MethodOverride accepts safe methods as targets. Restricting the override to legitimately overrideable methods (PUT, PATCH, DELETE) closes the bypass regardless of middleware ordering:

// method_override.go — illustrative fix
var allowedOverrideMethods = map[string]bool{
    http.MethodPut:    true,
    http.MethodPatch:  true,
    http.MethodDelete: true,
}

// in ToMiddleware:
if req.Method == http.MethodPost {
    m := config.Getter(c)
    if m != "" && allowedOverrideMethods[m] {
        req.Method = m
    }
}
Option 2: Use e.Pre() for MethodOverride and document the ordering requirement

Placing MethodOverride in e.Pre() (pre-routing middleware) causes the router to match the overridden method, not the original POST. If only POST /transfer is registered, a POST-to-GET override results in a 405/404 — the handler never executes. Add an explicit documentation warning that MethodOverride must not be placed in e.Use() when CSRF middleware is also registered:

// MethodOverride returns a MethodOverride middleware.
//
// IMPORTANT: Register this middleware with e.Pre(), not e.Use().
// If registered with e.Use() before CSRF middleware, method rewriting
// occurs after route matching but before CSRF validation, allowing
// attackers to bypass CSRF token checks by specifying _method=GET.
func MethodOverride() echo.MiddlewareFunc {

Option 1 is strongly preferred as it closes the vulnerability at the source and is safe regardless of middleware placement.

Credit

This vulnerability was discovered and reported by bugbunny.ai.

Patches

Fixed in github.com/labstack/echo/v5 v5.4.0 and github.com/labstack/echo/v4 v4.16.0. The MethodOverride middleware no longer overrides a POST to GET, HEAD, OPTIONS, TRACE or CONNECT.

Workarounds

Register MethodOverride with Echo#Pre (as documented), register CSRF before MethodOverride, or use the default header getter (X-HTTP-Method-Override), which a cross-site form cannot set.

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Echo's middleware.Static: %2F-encoded path bypasses route/group middleware guarding a statically-served path

CVE-2026-118650 / GHSA-375p-5qhx-8wq4

More information

Details

Summary

When middleware.Static is mounted globally (e.Use(middleware.Static(...))) and an application relies on a route- or group-level middleware guard to restrict a path that also falls under the served static root, a %2F-encoded request bypasses that guard while the file is still served. This is the same encoding/decoding mismatch fixed for StaticFS/StaticDirectoryHandler in CVE-2026-55677 / GHSA-vfp3-v2gw-7wfq, but the fix did not cover the middleware.Static path. In addition, the StaticConfig.EnablePathUnescaping docstring states the default configuration prevents exactly this bypass, which is inaccurate for the global-mount case.

This is not path traversal: no file outside the static root is accessed. The served file lives inside the static root; the issue is that access-control expectations set by a route guard are inconsistent between the literal and %2F-encoded forms of the same path.

Details

Root cause — the router and the static handler disagree on how to interpret %2F:

  • The router matches routes on the raw, still-encoded path, so GET /admin%2Fsecret.txt does not match a /admin/... route or group; any middleware attached to that route/group is therefore skipped.
  • middleware.Static selects its path source in middleware/static.go (~L219): with a global e.Use mount there is no wildcard route, so it uses c.Request().URL.Path, which net/http has already decoded (%2F → /). It then path.Cleans (~L235) and Opens (~L259) admin/secret.txt, serving the file.

Net effect: for the literal path the guard runs; for the %2F-encoded path the guard is skipped but the same file is served.

Relevant code:

  • middleware/static.go ~L219 (p := c.Request().URL.Path), ~L235 (path.Clean), ~L259 (currentFS.Open)
  • StaticConfig.EnablePathUnescaping docstring (static.go ~L60-67) — claims the default prevents the /admin%2fprivate.txt → /admin/* guard bypass; this is not true for the global mount.

Relationship to prior advisories:

  • CVE-2026-55677 / GHSA-vfp3-v2gw-7wfq (patched 5.2.0): same %2F router-vs-handler mismatch, but fixed for StaticFS/StaticDirectoryHandler. The middleware.Static path was not covered — this report is that gap.
  • CVE-2026-25766 / GHSA-pgvm-wxw2-hrv9 (patched 5.0.3): a different issue (Windows backslash traversal outside the root). Referenced only to disambiguate; not the same as this report.
PoC

Environment: golang:1.25, module using the official release (go get github.com/labstack/echo/v5@v5.3.1, no local replace). Static root public/ contains a marker file public/admin/secret.txt = SECRET.

Minimal app (the pattern under test: static served globally + a group guard intended to protect /admin):

package main

import (
	"net/http"

	"github.com/labstack/echo/v5"
	"github.com/labstack/echo/v5/middleware"
)

func main() {
	e := echo.New()
	e.Use(middleware.Static("public")) // serves ./public globally (default filesystem)

	admin := e.Group("/admin", func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c *echo.Context) error { return c.String(http.StatusUnauthorized, "auth required") }
	})
	admin.GET("/secret.txt", func(c *echo.Context) error { return c.String(http.StatusOK, "handler") })

	e.Start(":1323")
}

Requests (unauthenticated):


##### Literal: route matches -> group guard runs
curl -i http://localhost:1323/admin/secret.txt

##### => 401

##### %2F-encoded: route does NOT match -> guard skipped, but middleware.Static serves the file
curl -i --path-as-is http://localhost:1323/admin%2Fsecret.txt

##### => 200, body: SECRET

Observed on v5.3.1: literal → 401; %2F → 200 with the file contents. For contrast, serving via the (patched) e.StaticFS("/") returns 404 for the same %2F request, showing the middleware.Static path was not covered by the CVE-2026-55677 fix.

Impact

An unauthenticated remote user can retrieve a file served by middleware.Static even when the application attached a route/group middleware guard to that path, by substituting / with %2F. The consequence is limited to files already inside the served static root (CWE-863 authorization inconsistency; CWE-436 interpretation conflict) — there is no access to files outside the static root.

Honest caveat on validity / severity: this only has security impact for applications that (a) serve a directory statically to the public and (b) place a sensitive file inside that same directory while relying on a route-level guard to protect it. That combination is arguably a misconfiguration — a statically-served directory is public by design — so maintainers may reasonably classify this as a documentation fix (correct the EnablePathUnescaping claim) or won't-fix rather than a code vulnerability. It is filed primarily because (1) the router-vs-handler %2F inconsistency is the exact class fixed in CVE-2026-55677 but left uncovered for middleware.Static, and (2) the docstring currently promises protection it does not provide.

Patches

Fixed in github.com/labstack/echo/v5 v5.4.0 and github.com/labstack/echo/v4 v4.16.0. With the default settings, the Static middleware resolves files from the same form of the path that the router matched, so percent-encoded paths such as /admin%2Fsecret.txt or /%61dmin/secret.txt no longer reach a file under a guarded route. Paths with ., .. or empty segments are no longer served.

Note: middleware registered with Echo#Use runs before route and group middleware, so route guards never protect files served by a global Static middleware. Keep protected files outside its root directory, or serve them with Echo#Static behind the guard.

Workarounds

Keep files that need protection outside the Static middleware's root directory.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Echo vulnerable to Reflected Cross-Site Scripting (XSS) via unsanitized JSONP callback in Context.JSONP / Context.JSONPBlob

CVE-2026-118652 / GHSA-h9g5-28mm-hx3g

More information

Details

Summary

The Context.JSONP, Context.JSONPBlob, and internal jsonPBlob helper in context.go directly concatenate the caller-supplied callback parameter into a JavaScript response without any validation, escaping, or allow-listing.

This creates a high-impact reflected XSS that can be triggered by any route passing attacker-controlled data (query string, form, header, path param, etc.) into the callback.

Present in current master and v5.1.0 (latest release).

Vulnerable Code (verbatim)
func (c *Context) jsonPBlob(code int, callback string, i any) (err error) {
	c.writeContentType(MIMEApplicationJavaScriptCharsetUTF8)
	c.response.WriteHeader(code)
	if _, err = c.response.Write([]byte(callback + "(")); err != nil {
		return
	}
	if err = c.echo.JSONSerializer.Serialize(c, i, ""); err != nil {
		return
	}
	if _, err = c.response.Write([]byte(");")); err != nil {
		return
	}
	return
}

func (c *Context) JSONP(code int, callback string, i any) (err error) {
	return c.jsonPBlob(code, callback, i)
}

func (c *Context) JSONPBlob(code int, callback string, b []byte) (err error) {
	c.writeContentType(MIMEApplicationJavaScriptCharsetUTF8)
	c.response.WriteHeader(code)
	if _, err = c.response.Write([]byte(callback + "(")); err != nil {
		return
	}
	if _, err = c.response.Write(b); err != nil {
		return
	}
	_, err = c.response.Write([]byte(");"))
	return
}

No sanitization is performed on the callback string.

Exploit (one-request repro)

Any route using the official pattern is vulnerable:

Goe.GET("/api", func(c echo.Context) error {
    return c.JSONP(200, c.QueryParam("callback"), map[string]string{"status": "ok"})
})

Attacker request:

GET /api?callback=alert(document.domain)%2f%2f HTTP/1.1
Host: target.com

Server response:

Content-Type: application/javascript; charset=utf-8

alert(document.domain)//({"status":"ok"});

→ Immediate arbitrary JavaScript execution (full reflected XSS).

Impact
  • Session hijacking, cookie theft, account takeover, keylogging, defacement.
  • Works even with strict CSP (response is served as application/javascript).
Affected Versions
  • All versions ≤ v5.1.0 (including current master)
Workarounds

Manually sanitize the callback before calling JSONP/JSONPBlob:

Gofunc safeCallback(cb string) string {
    if matched, _ := regexp.MatchString(`^[a-zA-Z0-9_.\[\]]+$`, cb); !matched {
        return "callback"
    }
    return cb
}

Or migrate away from JSONP entirely (use CORS + application/json).

References
Patches

Fixed in github.com/labstack/echo/v5 v5.4.0 and github.com/labstack/echo/v4 v4.16.0. Context.JSONP and Context.JSONPBlob accept only a callback that is empty, a JavaScript identifier or a dot-separated path of identifiers (ASCII letters, digits, _ and $); any other callback returns a 400 Bad Request error wrapping ErrInvalidJSONPCallback and nothing is written. JSONP responses also carry X-Content-Type-Options: nosniff.

Workarounds

Validate the callback in your handler before calling Context.JSONP, for example against ^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$, and set X-Content-Type-Options: nosniff. JSONP responses can be read by any website with the user's cookies, so do not serve data that requires authentication through JSONP.

Severity

  • CVSS Score: 6.1 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Echo has Open Redirect via Control-Character Prefix Defeating sanitizeURI

CVE-2026-118656 / GHSA-v753-g4cw-jm48

More information

Details

Summary

Echo's dedicated open-redirect guard, sanitizeURI, decides whether a redirect target is a local path by inspecting exactly two byte positions: uri[0] and uri[1]. If the value begins with a control character such as a tab or a newline, followed by a separator pair, the guard does not fire and the value is written to the Location header unchanged.

Browsers remove tab and newline characters from URLs before parsing them, and treat a backslash as a forward slash for HTTP and HTTPS URLs. A Location value of /\t/attacker.example is therefore resolved by the browser as an absolute URL on attacker.example. Confirmed end to end in a headless browser.

Technical background
The guard

middleware/slash.go:144 (duplicated verbatim at echo.go:958):

func sanitizeURI(uri string) string {
	// double slash `\\`, `//` or even `\/` is absolute uri for browsers and by redirecting request to that uri
	// we are vulnerable to open redirect attack. so replace all slashes from the beginning with single slash
	if len(uri) > 1 && (uri[0] == '\\' || uri[0] == '/') && (uri[1] == '\\' || uri[1] == '/') {
		uri = "/" + strings.TrimLeft(uri, `/\`)
	}
	return uri
}

The condition is anchored at absolute offsets 0 and 1. It cannot see a separator that has been shifted by one byte.

Data flow
  1. RemoveTrailingSlash (middleware/slash.go:112-140) reads url.Path, which net/http has already percent-decoded, strips the trailing slash, and appends the raw query string:
path := url.Path
...
if l > 0 && strings.HasSuffix(path, "/") {
    path = path[:l]
    uri := path
    if qs != "" {
        uri += "?" + qs
    }
    if config.RedirectCode != 0 {
        return c.Redirect(config.RedirectCode, sanitizeURI(uri))   // line 132
    }
  1. AddTrailingSlash does the same in the opposite direction at middleware/slash.go:67.

  2. A request for /%09/attacker.example/x/ has url.Path equal to "/\t/attacker.example/x/" (a literal TAB byte). sanitizeURI evaluates uri[0] == '\t', the condition is false, and the value passes through untouched.

  3. Context.Redirect (context.go:735) writes it to the Location header. Go's header writer removes CR and LF but leaves TAB, which is all that is required.

Conditional analysis
  • Guard: uri[0] and uri[1] are separators. Trust boundary: server-side sanitization of attacker-influenced path data.
  • Bypass: place a single control byte before the separator pair. The separator moves to index 1 and the guard's second condition (uri[1] being a separator) is never reached because the first condition (uri[0] being a separator) already failed.
  • Browser-side trust boundary: the client's URL parser. It is adversary favourable: control characters are stripped and backslashes are normalized before scheme and authority parsing. The attacker does not need any server-side weakness beyond producing the malformed Location.

Note that the newline variant does not survive: net/http replaces CR and LF in header values. TAB does survive, and TAB alone is sufficient because the browser strips it.

Proof of concept

Attached PoC: ECHO-2026-002_poc.zip (in this directory). Extract it and follow
ECHO-2026-002/README.md for the full walkthrough. The archive contains a
self-contained Go module (go.mod, go.sum, main.go, README.md).

ECHO-2026-002_poc.zip

Quick start:

unzip ECHO-2026-002_poc.zip
cd ECHO-2026-002

##### edit go.mod: point the replace directive at your Echo checkout
sed -i 's|=> .*|=> /absolute/path/to/echo|' go.mod
GOFLAGS=-mod=mod go run .          # in-process check
GOFLAGS=-mod=mod go run . -serve   # real listener on 127.0.0.1:18002
Environment used for verification
  • OS: Linux 6.18.33.2-microsoft-standard-WSL2, x86_64
  • Go: go1.27.0 linux/amd64
  • Target: Echo v5.3.1 (module github.com/labstack/echo/v5)
  • Browser: headless Chromium drove the end-to-end confirmation
The vulnerable configuration
package main

import (
    "net/http"

    "github.com/labstack/echo/v5"
    "github.com/labstack/echo/v5/middleware"
)

func main() {
    e := echo.New()
    e.Pre(middleware.RemoveTrailingSlashWithConfig(middleware.RemoveTrailingSlashConfig{
        RedirectCode: http.StatusMovedPermanently,
    }))
    e.GET("/", func(c *echo.Context) error { return c.String(200, "home") })
    _ = e.Start("127.0.0.1:18002")
}

Reproduction

In-process check. The program requests five path shapes and applies the browser
URL-parsing steps that matter (strip TAB, LF, CR; treat a backslash as a slash; a
leading // starts an authority):

GOFLAGS=-mod=mod go run .
GET                          code   Location (control chars shown) browser navigates to
/normal/path/                301    /normal/path           (victim origin)
                             control: ordinary path, sanitized correctly
/%09/attacker.example/x/     301    /^I/attacker.example/x attacker.example
                             exploit: TAB then slash
/%09%5Cattacker.example/x/   301    /^I\attacker.example/x attacker.example
                             exploit: TAB then backslash
/..%09/attacker.example/     301    /..^I/attacker.example (victim origin)
                             NOT exploitable: TAB is not at index 1
/%09//attacker.example/      301    /^I//attacker.example  (victim origin)
                             NOT exploitable: three slashes is an empty authority

RESULT: reproducible. ...

^I is a literal TAB byte. The last two rows are deliberate negatives, included
so the mechanism (the guard reads only uri[0] and uri[1]) is unambiguous.

Over the wire, with the -serve variant running on 127.0.0.1:18002:

  1. Show the malformed Location on the wire. No backslash is needed for this variant:
curl -s -i --max-time 5 'http://127.0.0.1:18002/%09/attacker.example/x/' | cat -A | head -3

##### HTTP/1.1 301 Moved Permanently
##### Location: /^I/attacker.example/x

cat -A renders the tab as ^I, confirming it is a literal byte in the header.

  1. Show that LF does NOT work, which proves the mechanism is the guard's fixed
    byte offsets rather than generic header injection:
curl -s -i --max-time 5 'http://127.0.0.1:18002/%0A/attacker.example/x/' | cat -A | head -3

##### Location: / /attacker.example/x     (net/http rewrote LF to a space)
  1. Confirm a browser leaves the origin. Start any HTTP server on port 18099, then
    open this URL in a browser:
http://127.0.0.1:18002/%09/127.0.0.1:18099/x/

Observed in headless Chromium:

finalUrl: http://127.0.0.1:18099/x

The browser left the victim origin entirely, so this is a working open redirect,
not merely a suspicious header.

Exit status 0 means the finding reproduced; 1 means an expectation was
violated.

Impact

An attacker can craft a link on the victim domain that redirects the victim to an arbitrary external site. Practical consequences:

  • Phishing: the trusted domain name appears in the link and in the initial navigation, which defeats the usual "check the domain" advice.
  • OAuth and SSO token theft: any flow that permits a redirect target on the trusted origin and relies on that origin's redirects being local can be chained to leak authorization codes or access tokens.
  • Referrer and cache poisoning of downstream logic that treats the victim origin's redirects as trustworthy.

The client must follow the redirect (user interaction is required), which is why this is scored Medium rather than High, but open redirects on authentication-adjacent origins routinely escalate in practice.

Recommended fixes

Normalize before deciding, instead of probing two fixed offsets.

func sanitizeURI(uri string) string {
	// Strip control characters that browsers remove before parsing, so they cannot
	// be used to shift a separator out of the checked positions.
	uri = strings.Map(func(r rune) rune {
		if r < 0x20 || r == 0x7f {
			return -1
		}
		return r
	}, uri)
	// Collapse any leading separator run (both slash kinds) to a single slash.
	if strings.ContainsAny(uri[:min(len(uri), 1)], `/\`) {
		trimmed := strings.TrimLeft(uri, `/\`)
		if len(trimmed) != len(uri) {
			return "/" + trimmed
		}
	}
	return uri
}

Additional hardening:

  1. Reject the request with 400 when the decoded path contains control characters, rather than attempting to sanitize. A path containing a raw TAB or NUL is malformed under RFC 3986 and has no legitimate use.
  2. Apply the same normalization to the Location value in Context.Redirect so every caller benefits, instead of relying on each call site to call sanitizeURI.
  3. Add a regression test for each variant (%09/, %09%5C, %0A/) against both trailing-slash middlewares. There is currently no test that asserts sanitizeURI behavior at all.
Patches

Fixed in github.com/labstack/echo/v5 v5.4.0 and github.com/labstack/echo/v4 v4.16.0. The trailing slash middlewares and the static directory redirect percent-encode control characters in the redirect path before collapsing leading slashes, so a browser can no longer be sent to another host.

Workarounds

Use the trailing slash middlewares without RedirectCode (the default rewrites the path internally instead of redirecting), or reject request paths that contain control characters before these middlewares.

Severity

  • CVSS Score: 6.1 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Echo has Arbitrary Scheme Injection in WWWRedirect/NonWWWRedirect Middlewares via X-Forwarded-Proto

CVE-2026-118658 / GHSA-23wf-92cp-8pcj

More information

Details

Summary

The c.Scheme() method in Echo (context.go:162-181) trusts the X-Forwarded-Proto, X-Forwarded-Protocol, and X-Url-Scheme request headers without validating that the value is http or https. The WWWRedirect() and NonWWWRedirect() middlewares use this unsanitized scheme to construct redirect URLs, allowing an attacker to inject arbitrary URI schemes into the Location response header.

Affected Components
  • context.go:162-181 - Scheme() method returns attacker-controlled value verbatim
  • middleware/redirect.go:172-184 - redirectWWW/redirectNonWWW use unsanitized scheme
Proof of Concept
GET /path HTTP/1.1
Host: example.com
X-Forwarded-Proto: javascript

Response:
HTTP/1.1 301 Moved Permanently
Location: javascript://www.example.com/path

Confirmed injectable schemes: javascript, data, vbscript, file, ftp, ssh, telnet, gopher.

Only WWWRedirect() and NonWWWRedirect() (and WithConfig variants) are affected. HTTPSRedirect, HTTPSWWWRedirect, HTTPSNonWWWRedirect are NOT affected (they hardcode https://).

Impact
  • Open redirect to arbitrary scheme URLs when app is not behind a proxy that strips X-Forwarded-Proto
  • Cache poisoning when behind a CDN that caches redirect responses keyed by path
  • file:// scheme injection could trigger file access in some browser configurations
Suggested Fix

Whitelist scheme in c.Scheme() to only return http or https:

func (c *Context) Scheme() string {
    if c.IsTLS() {
        return "https"
    }
    if scheme := c.request.Header.Get(HeaderXForwardedProto); scheme != "" {
        scheme = strings.ToLower(strings.TrimSpace(scheme))
        if scheme == "https" {
            return "https"
        }
    }
    return "http"
}
Disclosure

Source code review, manually verified with actual HTTP server. Tested on Echo v5.0.4 (latest master).

Patches

Fixed in github.com/labstack/echo/v5 v5.1.1 and github.com/labstack/echo/v4 v4.15.2. Context.Scheme() now accepts only http, https, ws or wss from the X-Forwarded-Proto, X-Forwarded-Protocol and X-Url-Scheme headers, and falls back to http for any other value (#​2953).

Workarounds

Upgrade to a patched version. If you cannot upgrade, have your edge proxy overwrite or remove the X-Forwarded-Proto, X-Forwarded-Protocol and X-Url-Scheme request headers.

Severity

  • CVSS Score: 4.7 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Echo: HTTPS Redirect and HSTS Bypass via Unconditional Trust of Forwarded Scheme Headers

CVE-2026-118657 / GHSA-2ffq-g2xg-c22p

More information

Details

Description

Context.Scheme() unconditionally trusts four client-controlled HTTP headers to determine the request scheme, with no mechanism to configure trusted sources. This allows a remote unauthenticated attacker to bypass HTTPS redirect enforcement and suppress HSTS headers.

The trusted headers (checked in this order of precedence):

  1. X-Forwarded-Proto
  2. X-Forwarded-Protocol
  3. X-Forwarded-Ssl
  4. X-Url-Scheme
Threat model

Scenario A — Directly exposed application (vulnerable by default):
The Echo application listens on HTTP and is reachable directly by clients (no L7 proxy in front), or an attacker can bypass the proxy and reach the backend directly. In this case, the attacker has full control over all request headers. Sending X-Forwarded-Proto: https defeats the HTTPSRedirect middleware entirely — traffic stays on plaintext HTTP, enabling man-in-the-middle attacks.

This is the primary scenario. The application is vulnerable by default with no misconfiguration required — simply using e.Pre(middleware.HTTPSRedirect()) as documented is sufficient.

Scenario B — Behind L7 proxy with header sanitization (reduced risk):
If the application is strictly behind a correctly configured L7 proxy that always overwrites X-Forwarded-Proto and there is no direct access to the backend, the practical risk is lower. However, this relies on perimeter controls external to the framework, not on the framework itself. As Echo's own IP-extraction documentation states: "you must not trust HTTP headers unconditionally."

Root cause

context.go, lines 161–181:

func (c *Context) Scheme() string {
    if c.IsTLS() {
        return "https"
    }
    if scheme := c.request.Header.Get(HeaderXForwardedProto); scheme != "" {
        return scheme
    }
    if scheme := c.request.Header.Get(HeaderXForwardedProtocol); scheme != "" {
        return scheme
    }
    if ssl := c.request.Header.Get(HeaderXForwardedSsl); ssl == "on" {
        return "https"
    }
    if scheme := c.request.Header.Get(HeaderXUrlScheme); scheme != "" {
        return scheme
    }
    return "http"
}

Two additional issues with this implementation:

  1. No value validation. Scheme() returns any arbitrary string from the header, not just "http" or "https". This breaks the documented contract ("returns the HTTP protocol scheme, http or https") and causes unexpected behavior in middleware that uses the return value to construct URLs (e.g., WWWRedirect builds scheme + "://www." + host + uri).

  2. Consumed by security-critical middleware. The return value is used directly by redirect middleware (middleware/redirect.go:136) a

[!IMPORTANT]
✂ PR body was truncated to here.

@renovate

renovate Bot commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor Author

ℹ️ Artifact update notice

File name: go.mod

In order to perform the update(s) described in the table above, Renovate ran the go get command, which resulted in the following additional change(s):

  • 8 additional dependencies were updated
  • The go directive was updated for compatibility reasons

Details:

Package Change
go 1.26 -> 1.26.0
github.com/labstack/gommon v0.4.2 -> v0.5.0
github.com/mattn/go-colorable v0.1.14 -> v0.1.15
github.com/mattn/go-isatty v0.0.20 -> v0.0.22
golang.org/x/crypto v0.48.0 -> v0.57.0
golang.org/x/net v0.50.0 -> v0.60.0
golang.org/x/sys v0.41.0 -> v0.48.0
golang.org/x/text v0.34.0 -> v0.42.0
golang.org/x/time v0.14.0 -> v0.15.0

@codacy-production

codacy-production Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 23 high · 12 medium · 3 minor

Alerts:
⚠ 38 issues (≤ 0 issues of at least minor severity)

Results:
38 new issues

Category Results
Security 12 medium
3 minor
23 high

View in Codacy

🟢 Metrics 0 complexity · 0 duplication

Metric Results
Complexity 0
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@renovate renovate Bot changed the title Update module github.com/labstack/echo/v4 to v4.15.3 [SECURITY] Update module github.com/labstack/echo/v4 to v4.15.3 [SECURITY] - autoclosed Aug 30, 2026
@renovate renovate Bot closed this Aug 30, 2026
@renovate
renovate Bot deleted the renovate/go-github.com-labstack-echo-v4-vulnerability branch August 30, 2026 21:39
@renovate renovate Bot changed the title Update module github.com/labstack/echo/v4 to v4.15.3 [SECURITY] - autoclosed Update module github.com/labstack/echo/v4 to v4.15.3 [SECURITY] Aug 31, 2026
@renovate renovate Bot reopened this Aug 31, 2026
@renovate
renovate Bot force-pushed the renovate/go-github.com-labstack-echo-v4-vulnerability branch 2 times, most recently from d4b5a57 to b4af300 Compare August 31, 2026 01:02
@renovate renovate Bot changed the title Update module github.com/labstack/echo/v4 to v4.15.3 [SECURITY] Update module github.com/labstack/echo/v4 to v4.15.3 [SECURITY] - autoclosed Aug 31, 2026
@renovate renovate Bot closed this Aug 31, 2026
@renovate renovate Bot changed the title Update module github.com/labstack/echo/v4 to v4.15.3 [SECURITY] - autoclosed Update module github.com/labstack/echo/v4 to v4.15.3 [SECURITY] Aug 31, 2026
@renovate renovate Bot reopened this Aug 31, 2026
@renovate
renovate Bot force-pushed the renovate/go-github.com-labstack-echo-v4-vulnerability branch 2 times, most recently from b4af300 to 3489cb4 Compare August 31, 2026 17:49
@renovate
renovate Bot force-pushed the renovate/go-github.com-labstack-echo-v4-vulnerability branch from 3489cb4 to 2a18235 Compare October 28, 2026 20:07
@renovate renovate Bot changed the title Update module github.com/labstack/echo/v4 to v4.15.3 [SECURITY] Update module github.com/labstack/echo/v4 to v4.16.1 [SECURITY] Oct 28, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants